# encoding: UTF-8

require 'builder'

#noinspection RubyArgCount,RubyResolve,RubyLiteralArrayInspection
module Shapewear::WSDL
  # reference: http://www.w3.org/TR/wsdl
  def to_wsdl
    xm = Builder::XmlMarkup.new

    xm.instruct!
    xm.definitions :name => self.name, 'targetNamespace' => namespaces['tns'],
                   'xmlns' => namespaces['wsdl'],
                   'xmlns:soap' => namespaces['soap'],
                   'xmlns:soap12' => namespaces['soap12'],
                   'xmlns:xsd' => namespaces['xsd'],
                   'xmlns:tns' => namespaces['tns'] do |xdef|

      xdef.types do |xtypes|
        xtypes.schema 'xmlns' => namespaces['xsd'], :elementFormDefault => 'qualified', :targetNamespace => namespaces['tns'] do |xschema|

          # define elements for each defined method
          operations.each do |m, op_opts|
            build_type_elements_for_method(m, op_opts, xschema)
          end
        end
      end

      operations.each do |_, op_opts|
        xdef.message :name => "#{op_opts[:public_name]}SoapIn" do |xmsg|
          xmsg.part :name => :parameters, :element => "tns:#{op_opts[:public_name]}"
        end
        xdef.message :name => "#{op_opts[:public_name]}SoapOut" do |xmsg|
          xmsg.part :name => :parameters, :element => "tns:#{op_opts[:public_name]}Response"
        end
      end

      xdef.portType :name => "#{options[:service_name]}Soap" do |xpt|
        operations.each do |_, op_opts|
          xpt.operation :name => op_opts[:public_name] do |xop|
            xop.input :message => "tns:#{op_opts[:public_name]}SoapIn"
            xop.output :message => "tns:#{op_opts[:public_name]}SoapOut"
          end
        end
      end

      # bindings for soap 1.1 and 1.2
      ['', '12'].each do |sv|
        xdef.binding :name => "#{options[:service_name]}Soap#{sv}", :type => "tns:#{options[:service_name]}Soap" do |xbind|
          xbind.tag! "soap#{sv}:binding", :transport => 'http://schemas.xmlsoap.org/soap/http'
          operations.each do |_, op_opts|
            xbind.operation :name => op_opts[:public_name] do |xop|
              doc = op_opts[:documentation] rescue nil
              xop.documentation doc unless doc.nil?
              xop.tag! "soap#{sv}:operation", :soapAction => "#{namespaces['tns']}/#{op_opts[:public_name]}", :style => 'document'
              xop.input { |xin| xin.tag! "soap#{sv}:body", :use => 'literal' }
              xop.output { |xout| xout.tag! "soap#{sv}:body", :use => 'literal' }
            end
          end
        end
      end

      # service definition for soap 1.1 and 1.2
      xdef.service :name => "#{options[:service_name]}" do |xsrv|
        xsrv.documentation "WSDL auto-generated by shapewear."
        ['', '12'].each do |sv|
          xsrv.port :name => "#{options[:service_name]}Soap#{sv}", :binding => "tns:#{options[:service_name]}Soap#{sv}" do |xport|
            xport.tag! "soap#{sv}:address", :location => options[:endpoint_url]
          end
        end
      end
    end
  end

  def build_type_elements_for_method(m, op_options, xschema)
    # element for method arguments
    um = instance_method(m)

    xschema.element :name => "#{op_options[:public_name]}" do |xreq|
      xreq.complexType do |xct|
        xct.sequence do |xseq|
          params = op_options[:parameters] rescue nil
          if params.nil?
            if um.respond_to?(:parameters)
              # with Ruby 1.9, we can create the parameters with the correct names
              params = um.parameters.select { |p| p.first == :in }.map { |p| [p.first, Object] }
            else
              params = (0..um.arity).map { |i| ["arg#{i}", Object] }
            end
          end

          params.each do |p|
            t = p.last
            param_name = p.first
            param_name = param_name.camelize_if_symbol(:lower)
            if t.nil?
              xseq.element :name => param_name, :minOccurs => 0, :maxOccurs => 1, :type => 'xsd:any'
            elsif t.is_a?(Class)
              xseq.element :name => param_name, :minOccurs => 0, :maxOccurs => 1, :type => to_xsd_type(t)
            elsif t.is_a?(Hash)
              xseq.complexType do |xct2|
                xct2.sequence do |xseq2|
                  t.each do |name, type|
                    xseq2.element :name => name, :minOccurs => 0, :maxOccurs => 1, :type => to_xsd_type(type)
                  end
                end
              end
            else
              raise "Could not interpret #{t.inspect} as a return type definition"
            end
          end
        end
      end
    end

    # element for method result
    ret = op_options[:returns] rescue nil

    xschema.element :name => "#{op_options[:public_name]}Response" do |xreq|
      xreq.complexType do |xct|
        xct.sequence do |xseq|
          xseq.element :name => "#{op_options[:public_name]}Result", :minOccurs => 0, :maxOccurs => 1,
                       :type => case ret
                         when NilClass then 'xsd:any'
                         when Class then to_xsd_type(ret)
                         when Hash then "tns:#{op_options[:public_name]}Struct"
                         else
                           raise "Could not interpret #{ret.inspect} as a return type definition"
                       end
        end
      end
    end

    if ret.is_a?(Hash)
      xschema.complexType :name => "#{op_options[:public_name]}Struct" do |xctr|
        xctr.sequence do |xseqr|
          ret.each do |name, type|
            name = name.camelize_if_symbol
            xseqr.element :name => name, :minOccurs => 0, :maxOccurs => 1, :type => to_xsd_type(type)
          end
        end
      end
    end
  end

  # @param t [Class]
  def to_xsd_type(t)
    return 'xsd:string' if t == String
    return 'xsd:integer' if t == Fixnum
    return 'xsd:dateTime' if t == DateTime
    return 'xsd:any' if t.nil? || t == Object
    raise "Could not convert type #{t} to a valid XSD type"
  end
end
